package org.softee.extractor.sax.helper;

import static org.softee.extractor.sax.helper.XmlExtractor.State.INSIDE;
import static org.softee.extractor.sax.helper.XmlExtractor.State.OUTSIDE;

import java.io.IOException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;

import org.softee.extractor.QNameMatcher;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class XmlExtractor<T> implements ContentHandler, LexicalHandler {
    protected enum State {
        OUTSIDE, INSIDE
    };
    private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();

    private State state;
    private final QNameMatcher qNameMatcher;
    private final ResultHandler<T> resultHandler;
    private int tagLevel;

    private final ResultBuildingContentHandler<T> resultBuilder;

    public XmlExtractor(QNameMatcher qNameMatcher, ResultHandler<T> resultHandler,
            ResultBuildingContentHandler<T> resultBuilder) throws ParserConfigurationException,
            SAXNotRecognizedException, SAXNotSupportedException {
        this.qNameMatcher = qNameMatcher;
        this.resultHandler = resultHandler;
        this.resultBuilder = resultBuilder;
        state = OUTSIDE;
    }

    public void process(InputSource inputSource) throws IOException, SAXException {
        XMLReader xmlReader = XMLReaderFactory.createXMLReader();
        xmlReader.setContentHandler(this);
        try {
            // according to LexicalHandler JavaDoc, it
            // "is an optional extension handler for SAX2".
            xmlReader.setProperty(XmlConstants.LEXICAL_HANDLER, this);
        } catch (SAXNotRecognizedException ignore) {
            /*
             * The SAX parser does not support a Lexical event handler, which
             * means that XML Comments and CDATA delimiters will not be handled
             * and passed on to the target documents
             */
        }

        // FIXME set DTDHandler, ErrorHandler, EntityResolver on xmlReader?
        xmlReader.parse(inputSource);
    }

    public void process(Source source) throws TransformerException {
        SAXResult result = new SAXResult();
        result.setHandler(this);
        result.setLexicalHandler(this);
        transformerFactory.newTransformer().transform(source, result);
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        // FIXME forward?
    }

    @Override
    public void startDocument() throws SAXException {
        // deliberately ignored
    }

    @Override
    public void endDocument() throws SAXException {
        // deliberately ignored
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if (state == OUTSIDE) {
            if (qNameMatcher.matches(uri, localName)) {
                resultBuilder.startDocument();
                state = INSIDE;
                tagLevel = 0;
            }
        }
        if (state == INSIDE) {
            resultBuilder.startElement(uri, localName, qName, atts);
            tagLevel++;
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.endElement(uri, localName, qName);
            if (--tagLevel == 0) {
                // we have reached the end of the root element of the extracted
                // fragment
                // simulate an endDocument event to the result builder
                resultBuilder.endDocument();
                resultHandler.accept(resultBuilder.getResult());
                state = OUTSIDE;
            }
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.characters(ch, start, length);
        }
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.ignorableWhitespace(ch, start, length);
        }
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        resultBuilder.startPrefixMapping(prefix, uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException {
        resultBuilder.endPrefixMapping(prefix);
    }

    @Override
    public void processingInstruction(String target, String data) throws SAXException {
        // FIXME forward
    }

    @Override
    public void skippedEntity(String name) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.skippedEntity(name);
        }
    }

    @Override
    public void comment(char[] ch, int start, int length) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.comment(ch, start, length);
        }
    }

    @Override
    public void startCDATA() throws SAXException {
        if (state == INSIDE) {
            resultBuilder.startCDATA();
        }
    }

    @Override
    public void endCDATA() throws SAXException {
        if (state == INSIDE) {
            resultBuilder.endCDATA();
        }
    }

    @Override
    public void startEntity(String name) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.startEntity(name);
        }
    }

    @Override
    public void endEntity(String name) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.endEntity(name);
        }
    }

    @Override
    public void startDTD(String name, String publicId, String systemId) throws SAXException {
        if (state == INSIDE) {
            resultBuilder.startDTD(name, publicId, systemId);
        }
    }

    @Override
    public void endDTD() throws SAXException {
        if (state == INSIDE) {
            resultBuilder.endDTD();
        }
    }
}
